package fr.uvsq.isty.bdh.hchess.interfacing;

import ictk.boardgame.AmbiguousMoveException;
import ictk.boardgame.IllegalMoveException;
import ictk.boardgame.chess.ChessBoard;
import ictk.boardgame.chess.ChessGame;
import ictk.boardgame.chess.ChessGameInfo;
import ictk.boardgame.chess.ChessMove;
import java.io.IOException;
import java.io.PipedWriter;


/**
 * This class takes two chess players, and makes them play together.
 * The game is ran in an other thread. The pipeWriter given in the constructor
 * is used to send the game state and updates.
 * @author Clement Bellot <clementbellot@gmail.com>
 */
public class Referee extends Thread {
    private IChessPlayer whitePlayer;
    private IChessPlayer blackPlayer;
    private ChessGame game;
    private int[][] stateToRestore;
    private final PipedWriter gameDump;
    private boolean ran = false;
    private boolean drawByAgreement;
    private boolean surrendered;
    private boolean whiteAskedDraw;
    private boolean blackAskedDraw;
    private boolean winner;
    private boolean playerCommunicationError;
    private boolean whiteWasSetup;
    private boolean blackWasSetup;
    public static final boolean WHITE = false;
    public static final boolean BLACK = true;
    
    public static final char PIPE_MOVE = 'm';
    public static final char PIPE_EVENT = 'e';
    public static final char PIPE_BOARD = 'b';
    
    /**
     * Creates an instance of a referee, with the default board position
     * @param whitePlayer The player playing whites.
     * Must not be already in game.
     * @param blackPlayer The player playing blacks.
     * Must not be already in game.
     * @param gameDump A connected PipedWriter in which will be dumped: the
     * starting board, and then the moves player. At any time an event can also
     * be sent trough it.
     * The formating for the board and moves can be interpreted with
     * Helpers.fixedSizeSerializeBoard and fixedSizeSerializeMove . You can get
     * the size serialized with Helpers.getMoveSizeFixedSerialized and 
     * Helpers.getBoardSizeFixedSerialized .
     * For the events, one char is written, corresponding to the 
     * <code>ordinal()</code> of the GameEvent.
     */
    public Referee(
            IChessPlayer whitePlayer,
            IChessPlayer blackPlayer,
            PipedWriter gameDump){
        assert whitePlayer!=null  && blackPlayer!= null :
                "Stop sending me junk.";
        this.whitePlayer = whitePlayer;
        this.blackPlayer = blackPlayer;
        this.game = new ChessGame();
        this.gameDump = gameDump;
        this.ran = false;
        this.drawByAgreement = false;
        this.surrendered = false;
        this.winner = false;// Not yet used, only if surendered is true.
        this.whiteAskedDraw = false;
        this.blackAskedDraw = false;
        this.whiteWasSetup = false;
        this.blackWasSetup = false;
        this.playerCommunicationError = false;
        this.stateToRestore = new int[0][];
        
    }
    public Referee(
            IChessPlayer whitePlayer,
            IChessPlayer blackPlayer,
            PipedWriter gameDump,
            ChessBoard board,
            int[][] stateToRestore){
        this(whitePlayer,blackPlayer,gameDump);
        this.game = new ChessGame(new ChessGameInfo(), board);
        if(stateToRestore==null){
            stateToRestore = new int[0][];
        }
        this.stateToRestore = stateToRestore;
    }
    private GameEvent getGameEndReason(ChessGame game){
        return null;
    }
    /**
     * Notifies the interface of a player event.
     * @param from The player.
     * @param event The event to pass.
     */
    public void reciveEvent(boolean from, GameEvent event) {
        try {
            // Transmit it to the other player.
            sendEvent(!from,event);
            if(event==GameEvent.BLACK_ASKED_DRAW){
                blackAskedDraw = true;
            }
            if(event==GameEvent.WHITE_ASKED_DRAW){
                whiteAskedDraw = true;
            }
            if(event==GameEvent.BLACK_ACCEPTED_DRAW && whiteAskedDraw){
                sendEvent(!from,GameEvent.DRAW_BY_AGREEMENT);
            }
            if(event==GameEvent.WHITE_ACCEPTED_DRAW && blackAskedDraw){
                sendEvent(!from,GameEvent.DRAW_BY_AGREEMENT);
                drawByAgreement = true;
            }
            if(event==GameEvent.BLACK_REFUSED_DRAW && whiteAskedDraw){
                whiteAskedDraw = false;
            }
            if(event==GameEvent.WHITE_REFUSED_DRAW && blackAskedDraw){
                blackAskedDraw = false;
            }
            if(event==GameEvent.WHITE_CANCELED_DRAW && whiteAskedDraw){
                whiteAskedDraw = false;
            }
            if(event==GameEvent.BLACK_ASKED_DRAW && blackAskedDraw){
                blackAskedDraw = false;
            }
            if(event==GameEvent.WHITE_SURRENDERED && from == WHITE){
                surrendered = true;
                winner = BLACK;
                blackPlayer.notifyGameEvent(GameEvent.WHITE_SURRENDERED);
                try {
                    writeToGameDump(
                            PIPE_EVENT,
                            ""+(char)(GameEvent.WHITE_SURRENDERED.ordinal()));
                } catch (IOException ex) {}
            }
            if(event==GameEvent.BLACK_SURRENDERED && from == BLACK){
                surrendered = true;
                winner = WHITE;
                blackPlayer.notifyGameEvent(GameEvent.BLACK_SURRENDERED);
                try {
                    writeToGameDump(
                            PIPE_EVENT,
                            ""+(char)(GameEvent.BLACK_SURRENDERED.ordinal()));
                } catch (IOException ex) {}
            }
        } catch (FailedToReachPlayerException ex) {
            playerCommunicationError = true;
        }
    }
    private void sendEvent(boolean to, GameEvent event) throws FailedToReachPlayerException{
        if(to==WHITE){
            whitePlayer.notifyGameEvent(event);
        }else{
            blackPlayer.notifyGameEvent(event);
        }
    }
    private void writeToGameDump(int type, String data) throws IOException{
        synchronized(gameDump){
            if(type==PIPE_MOVE){
                gameDump.write((char)PIPE_MOVE);
            }else if(type==PIPE_EVENT){
                gameDump.write((char)PIPE_EVENT);
            }else if(type==PIPE_BOARD){
                gameDump.write((char)PIPE_BOARD);
            }
            gameDump.write(data);
        }
    }
    private ChessMove playMove(int[] lastMove){
        ChessMove lastMoveOnBoard = null;
        try {
            if(lastMove.length==5){
                lastMoveOnBoard = new ChessMove(
                        (ChessBoard)game.getBoard(),
                        lastMove[0],lastMove[1],
                        lastMove[2],lastMove[3],
                        lastMove[4]);
            }else{
                lastMoveOnBoard = new ChessMove(
                        (ChessBoard)game.getBoard(),
                        lastMove[0],lastMove[1],
                        lastMove[2],lastMove[3]);
            }
            game.getHistory().add(lastMoveOnBoard);
        } catch (IllegalMoveException ex) {
            
            return null;
        } catch (IndexOutOfBoundsException ex) {
            return null;
        } catch (AmbiguousMoveException ex) {
            assert false : ex.getMessage();
        }
        
        return lastMoveOnBoard;
    }
    private void handlePlayerDisconnection(boolean currentPlayer){
        boolean lostConnectionWithWhite = false;
        boolean lostConnectionWithBlack = false;
        if(currentPlayer == WHITE){
            lostConnectionWithWhite = true;
            if(blackWasSetup){
                try{
                    blackPlayer.notifyGameEvent(
                            GameEvent.WHITE_LOST_CONNECTION);
                }catch(FailedToReachPlayerException e){
                    lostConnectionWithBlack = true;
                }
            }
        }else{
            lostConnectionWithBlack = true;
            if(whiteWasSetup){
                try{
                    whitePlayer.notifyGameEvent(
                            GameEvent.BLACK_LOST_CONNECTION);
                }catch(FailedToReachPlayerException e){
                    lostConnectionWithWhite = true;
                }
            }
        }
        try{
            if(lostConnectionWithWhite){
                writeToGameDump(
                        PIPE_EVENT,
                        ""+(char)(GameEvent.WHITE_LOST_CONNECTION.ordinal())
                        );
            }
            if(lostConnectionWithBlack){
                writeToGameDump(
                        PIPE_EVENT,
                        ""+(char)(GameEvent.BLACK_LOST_CONNECTION.ordinal())
                        );
            }
        }catch(IOException e){
            // Big problem, the out pipe is lost. And we can't do anything
            // about it.
        }
    }
    @Override
    public void run(){
        assert !ran : "Thou shalt not call me twice";
        ran = true;
        boolean currentPlayer = WHITE;// false stands for white.
        try{
            currentPlayer = WHITE;
            whitePlayer.setUp(
                    new GameEventRefereeNotifier(this, WHITE),
                    (ChessBoard)game.getBoard(),
                    stateToRestore);
            currentPlayer = BLACK;
            whiteWasSetup = true;
            blackPlayer.setUp(
                    new GameEventRefereeNotifier(this, BLACK),
                    (ChessBoard)game.getBoard(),
                    stateToRestore);
            blackWasSetup = true;
            currentPlayer = WHITE;
            boolean playerDidBadMove = false;
            int[] lastMove = null;
            ChessMove lastMoveOnBoard = null;
            writeToGameDump(
                    PIPE_BOARD,
                    Helpers.fixedSizeSerializeBoard(
                        (ChessBoard)game.getBoard()));
            // Restoring state, if needed.
            for(int[] move : stateToRestore){
                lastMoveOnBoard = playMove(move);
                if(lastMoveOnBoard == null){
                    throw new IllegalArgumentException(
                            "The state to you once gave me was not valid.");
                }
                writeToGameDump(
                        PIPE_MOVE,
                        Helpers.fixedSizeSerializeMove(lastMoveOnBoard));
                currentPlayer = !currentPlayer;
                lastMove = move;
            }
            writeToGameDump(
                    PIPE_EVENT,
                    ""+(char)(GameEvent.REFEREE_STATE_RESTORED.ordinal()));
            
            // Playing loop.
            do{
                if(currentPlayer == WHITE) {
                    lastMove = whitePlayer.getNextMove(lastMove);
                }else{
                    lastMove = blackPlayer.getNextMove(lastMove);
                }
                if(surrendered){
                    break;
                }
                lastMoveOnBoard = playMove(lastMove);
                if(lastMoveOnBoard==null){
                    playerDidBadMove = true;
                    break;
                }
                writeToGameDump(
                        PIPE_MOVE,
                        Helpers.fixedSizeSerializeMove(lastMoveOnBoard));
                System.out.println(lastMoveOnBoard);
                currentPlayer = !currentPlayer;
            }while(!lastMoveOnBoard.isEndOfGame() && !drawByAgreement);
            //
            // Game is over,  getting game end reason and comunicating it.
            GameEvent endEvent = null;
            if(playerDidBadMove){
                if(currentPlayer==BLACK){
                    endEvent = GameEvent.BLACK_BAD_MOVE;
                }else{
                    endEvent = GameEvent.WHITE_BAD_MOVE;
                }
            }else if(surrendered){
                // Already handled.
            }else if(lastMoveOnBoard.isCheckmate()){
                if(currentPlayer==BLACK){// Inverted due to player switch.
                    endEvent = GameEvent.WHITE_CHECKMATE;
                }else{
                    endEvent = GameEvent.BLACK_CHECKMATE;
                }
            }else if(lastMoveOnBoard.isStalemate()){
                endEvent = GameEvent.STALEMATE;
            }else if(drawByAgreement){
                endEvent = GameEvent.DRAW_BY_AGREEMENT;
            }else{
                assert false : "Some piece of code is missing, somewhere.";
            }
            // Updating the last move.
            if(currentPlayer==WHITE){
                whitePlayer.notifyFinalMove(lastMove);
            }else{
                blackPlayer.notifyFinalMove(lastMove);
            }
            // Notify event.
            if(endEvent!=null){
                whitePlayer.notifyGameEvent(endEvent);
                blackPlayer.notifyGameEvent(endEvent);
                writeToGameDump(PIPE_EVENT, ""+(char)(endEvent.ordinal()));
            }
        }catch(FailedToReachPlayerException ex){
            //ex.printStackTrace();
            handlePlayerDisconnection(currentPlayer);
        }catch(IOException ex){
            ex.printStackTrace();
            assert false : "Too lazy to handle this case.";
        }
        try {
            writeToGameDump(PIPE_EVENT,
                    ""+(char)(GameEvent.REFEREE_EXITING.ordinal()));
        } catch (IOException ex) {
        }
    }
}
